"""
$Id: parser.py 2 2007-03-04 19:08:44Z sidnei.da.silva $
"""

import os
import thread
from concurrency import History, Transaction, Operation
from cStringIO import StringIO

# Some constants
XMLNS_NS = 'http://www.w3.org/2000/xmlns/'
XMLREADER_START_ELEMENT_NODE_TYPE = 1
XMLREADER_END_ELEMENT_NODE_TYPE = 15
EXEC_NS = 'http://awkly.org/ns/sgbd/concurrency/'

# Initialize RNG Schema
CUR_DIR = os.path.dirname(__file__)
RNG_FILE = open(os.path.join(CUR_DIR, 'validation', 'execution.rng'), 'rb')
RNG = RNG_FILE.read(); RNG_FILE.close()
del RNG_FILE

# libxml2 initialization. Register a per-thread global error callback.
import libxml2
libxml2.initParser()

class ErrorCallback:

    def __init__(self):
        self.msgs = {}

    def __call__(self, ctx, msg):
        self.append(msg)

    def append(self, msg):
        tid = thread.get_ident()
        msgs = self.msgs.setdefault(tid, [])
        msgs.append(msg)

    def get(self, clear=False):
        tid = thread.get_ident()
        msgs = self.msgs.setdefault(tid, [])
        if clear: self.clear()
        return ''.join(msgs)

    def clear(self):
        tid = thread.get_ident()
        msgs = self.msgs[tid] = []

error_callback = ErrorCallback()
libxml2.registerErrorHandler(error_callback, "")

class ParserError(Exception): pass

def findId(reader):
    while reader.MoveToNextAttribute():
        if reader.LocalName() == 'id':
            return reader.Value()
    return None

def parse(infile):
    error_callback.clear()
    # libxml2.debugMemory(1)
    libxml2.lineNumbersDefault(1)
    input_source = libxml2.inputBuffer(infile)
    reader = input_source.newTextReader("urn:foo")

    # Initialize RNG schema validation.
    rngp = libxml2.relaxNGNewMemParserCtxt(RNG, len(RNG))
    rngs = rngp.relaxNGParse()
    reader.RelaxNGSetSchema(rngs)

    history = History()
    transactions = {}
    transaction = None
    tid = None
    o_tid = None
    ret = reader.Read()
    read = False
    is_valid = True
    msg = ''
    while ret == 1 and is_valid:
        if read:
            ret = reader.Read()
        read = True
        if reader.NodeType() == XMLREADER_END_ELEMENT_NODE_TYPE:
            if reader.LocalName() in ('transaction',):
                # Reset current transaction id
                tid = None
        if reader.NodeType() == XMLREADER_START_ELEMENT_NODE_TYPE:
            if reader.NamespaceUri() != EXEC_NS:
                continue
            if reader.LocalName() in ('execution',):
                # It's the outermost element
                continue
            if reader.LocalName() in ('history',):
                # Find history id
                history.id = findId(reader)
                continue
            if reader.LocalName() in ('transaction',):
                # Set current transaction id
                tid = findId(reader)
                continue
            # All 'special' elements were handled. Now to handle the operations.
            operation = Operation(type=reader.LocalName())
            while reader.MoveToNextAttribute():
                operation[reader.LocalName()] = reader.Value()
                if reader.LocalName() in ('transaction',):
                    o_tid = reader.Value()
                    if tid is not None and o_tid != tid:
                        is_valid = False
                        msg = ("Transaction ID in Operation doesn't "
                               "match current Transaction")
                    if transactions.has_key(o_tid):
                        transaction = transactions[o_tid]
                    else:
                        transactions[o_tid] = transaction = Transaction(o_tid)

            transaction.append(operation)
            if tid is None:
                # history-mode
                history.append(operation)

    if history.id is None:
        # transaction-mode
        history.update(*transactions.values())
    else:
        # history-mode
        history.set_transactions(transactions)

    is_input_valid = reader.IsValid()
    # libxml2 cleanups. Don't keep references around
    # for longer than we need.
    del rngp
    del rngs
    del reader
    del input_source
    libxml2.relaxNGCleanupTypes()
    # Memory debug specific
    libxml2.cleanupParser()
    # Useful for debugging memory errors
    # if libxml2.debugMemory(1) == 0:
    #    print "OK"
    # else:
    #    print "Memory leak %d bytes" % (libxml2.debugMemory(1))
    # libxml2.dumpMemory()

    if not is_valid:
        raise ParserError, msg

    if ret != 0:
        errors = error_callback.get(clear=True)
        raise ParserError, ("There was an error parsing the "
                            "input. Please make sure that it "
                            "is well formed and try again.\n"
                            "%s" % errors)
    if is_input_valid != 1:
        errors = error_callback.get(clear=True)
        raise ParserError, ("Input failed to validate against "
                            "the RelaxNG schema.\n"
                            "%s" % errors)
    error_callback.clear()

    return history
